/*
 * Trigger.java
 *
 * Created on June 27, 2007, 2:28 AM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.matchers;

import tal.drivechain.chain.ChainNode;
import tal.drivechain.wildcard.StringElement;
import tal.drivechain.wildcard.ChannelElement;
import tal.drivechain.wildcard.Wildcard.Operator;
import tal.drivechain.parsing.Tokenizer;
import tal.drivechain.plugin.Channel;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.Variable;
import tal.drivechain.wildcard.Wildcard;

/**
 * This class will check a {@link Variable} object against a matching pattern. 
 * The format of the pattern is varname:(vartype)matching-pattern.
 * varname can be a variable name or "any" if any variable name is allowed.
 * vartype is a Variable type name or "any" if any variable data type is allowed.
 * The matching-pattern is different for different vartype values. See {@link ValueMatcher}
 * for more information. If vartype is any adding a matching-pattern would throw an exception.
 * <p>
 * VarMatcher also supports array matching. if the datatype is an array (using the [] identifier)
 * the matching-pattern should be a list of patterns for the type of the array delimited by a colon.
 * If the checked Variable has more elements than supplied in the definition, they will be checked against 
 * a pattern of "any". 
 * <p>
 * Examples:
 * any:(int)[0..5] will match any variable of type Variable.TYPE_INT with values between 0 and 5.
 * any:(midi[])noteon/[c3,c4],noteoff/[c3,c4] will match any array variable of type Variable.TYPE_MIDI
 * with the first element matching a noteon message with either c3 or c4 as it's data1 and the second
 * element matching a noteoff message with the same values for data1. The matcher will ignore any
 * other elements.
 * in:(any[]) will match a variable named "in" with any array variable type.
 * <p>
 * 
 * @author Tal Eisenberg
 */
public class VarMatcher extends ValueMatcher {
    DataType varType;
    ValueMatcher valMatcher;
    Wildcard varName = null;
    Wildcard channel = null;    
    
    /** 
     * Creates a new instance of VarMatcher with the supplied definition string.
     * 
     * @param def   The definition string of the matcher.
     * @throws IllegalArgumentException If encountered an error while parsing the definition.
     */
    protected void parseSpecial(String def) throws IllegalArgumentException {
        // format varname/channel(vartype:matching-pattern)
        // get varType and isArray; TYPE_UNKNWON for any don't forget any[] (isArray=true)
        // in case of a wildcard store it in wildcards
        // store propertyName if needed
        // store varName if vartype is not a type
        // when matching string store the whole pattern as a p.compile(pattern);
        // any must not to have a pattern
        def = def.trim();
        int slashIdx = def.indexOf('/');
        int openparIdx = def.indexOf('(', slashIdx+1);

        String svarname = null;
        String schannel = null;
        String svartype = null;
        String spattern = null;

        if (openparIdx==-1) { // no vartype:matching-pattern
            if (slashIdx==-1) // no channel {
                svarname = def;
            else {
                svarname = def.substring(0, slashIdx);
                schannel = def.substring(slashIdx+1);
            }
        } else { // has vartype and optionally matching-pattern {
            //check for missing closing parens.
            if (Tokenizer.balanced(def, openparIdx, '(', ')')!=def.length()-1)
                throw new IllegalArgumentException("Missing closing parentheses: " + def);
            if (slashIdx==-1) { // no channel
                svarname = def.substring(0, openparIdx);
            } else {
                svarname = def.substring(0, slashIdx);
                schannel = def.substring(slashIdx+1, openparIdx);
            }

            String varmatcher = def.substring(openparIdx+1, def.length()-1);
            int colonIdx = varmatcher.indexOf(":");
            if (colonIdx==-1) svartype = varmatcher;
            else {
                svartype = varmatcher.substring(0, colonIdx);
                spattern = varmatcher.substring(colonIdx+1);
            }
        }

        if (svarname.equals("any")) varName = null;
        else varName = Wildcard.parseDefinition(svarname, new StringElement());

        if (schannel!=null)
            channel = Wildcard.parseDefinition(schannel, new ChannelElement());
        else channel = new Wildcard(null, null, Wildcard.Operator.ANY);

        if (svartype==null || svartype.equals("any") || svartype.equals("any[]")) varType = null;
        else {
            varType = DataType.parseType(svartype);
            if (varType.matches(DataType.EXPR)) throw new IllegalArgumentException("expr data type can't be used in a matcher. use the return type instead.");
        }

        if (spattern==null || varType==null) valMatcher = null;
        else {
            valMatcher = varType.createValueMatcher();
            valMatcher.parsePattern(spattern);
        }
    }
    
    /**
     * Returns true if the supplied {@link Variable} matches the object's matching definiton.
     * 
     * @param var   The varaible that will be checked against the definition.
     * @param localVars A VarTable that will be used to evaluate the patterns if necessary.
     * @throws IllegalArgumentException If encountered an error while checking the pattern or
     *                                  if var is null.
     */
    protected boolean matchSpecial(Object value, ChainNode context) throws IllegalArgumentException{
        Object[] msg = (Object[])value;
        Variable var = (Variable)msg[0];
        Channel channelType = (Channel)msg[1];
        if (var==null) throw new IllegalArgumentException("Can't pass a null variable to variable matcher.");
        // first check datatype if varType!=TYPE_UNKNOWN
        // check isArray
        // check matchers
        if (varName!=null && !varName.matches(var.getName(), context)) return false;
        if (channel!=null && channelType!=null && !channel.matches(channelType, context)) return false;
        if (varType!=null && !var.getDataType().matches(varType)) return false;
        if (valMatcher==null) return true;
        else return valMatcher.matches(var.getValue(context), context);
    }
    
    @Override
    public String toString() {
        if (any) return "any";
        if (none) return "none";
        else {
            String ret;
            if (varName!=null) ret = varName.toString();
            else ret = "any";
            if (channel!=null && channel.getOperator()!=Operator.ANY) {
                String channelString = channel.toString();
                ret += "/" + channelString;
            }
            if (varType!=null) {
                ret += "(" + varType.getName();
                if (valMatcher!=null)
                    ret += ":" + valMatcher.toString();
                ret += ")";
            }
            return ret;
        }
    }
    
    public boolean matches(Variable var, Channel channelType, ChainNode context) {
        return matches(new Object[] {var, channelType}, context);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof VarMatcher) {
            if (super.equals(obj)) return true;
            else {
                VarMatcher that = (VarMatcher)obj;
                if (this.channel==null && that.channel!=null) return false;
                if (this.channel!=null && that.channel==null) return false;
                if (channel!=null && !this.channel.equals(that.channel)) return false;

                if (this.valMatcher==null && that.valMatcher!=null) return false;
                if (this.valMatcher!=null && that.valMatcher==null) return false;
                if (valMatcher!=null && !this.valMatcher.equals(that.valMatcher)) return false;

                if (this.varName==null && that.varName!=null) return false;
                if (this.varName!=null && that.varName==null) return false;
                if (varName!=null && !this.varName.equals(that.varName)) return false;

                if (this.varType==null && that.varType!=null) return false;
                if (this.varType!=null && that.varType==null) return false;
                if (varType!=null && !this.varType.matches(that.varType)) return false;

                return true;
            }
        } else return false;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 79 * hash + (this.varType != null ? this.varType.hashCode() : 0);
        hash = 79 * hash + (this.valMatcher != null ? this.valMatcher.hashCode() : 0);
        hash = 79 * hash + (this.varName != null ? this.varName.hashCode() : 0);
        hash = 79 * hash + (this.channel != null ? this.channel.hashCode() : 0);
        return hash;
    }

}
